
#include "nu_api.h"
#include "tapi_ppp.h"
#include "ddk.h"

EXTERN_C int CDECL _sprintf(char *buf, const char *fmt,...);

extern "C" void tcpip_rawip_set_handler(
   void (*send_ip)(int id, void *buffer_ptr, unsigned long size),
   void (*recv_ip)(int *id, void **buffer_ptr_ptr, unsigned long *size),
   void (*free_ip)(void *buffer_ptr));


#define MAX_CONN_COUNT                              4
#define MAX_BUFFER_COUNT                            50

enum CONNECT_STAT {
    disconnected,
    connecting,
    connected,
    disconnecting,
};

struct IP_PACKET {
    void *buffer;
    unsigned long size;
};

struct IP_PACKET_QUEUE {
    int count;
    int length;
    int put, get;
    struct IP_PACKET packet[MAX_BUFFER_COUNT];
    unsigned char state;
    unsigned char cid;
    unsigned long fluxRecvCount;
    unsigned long fluxSendCount;
};

struct IP_CONN {
    IP_CONN() :
        dataRecvEvent(FALSE, UNSIGNALED),
        dataSendEvent(FALSE, UNSIGNALED),
        connectEvent(FALSE, UNSIGNALED) {}
    struct IP_PACKET_QUEUE ipQueue[MAX_CONN_COUNT];
    int count;
    int lock;
    DzEvent dataRecvEvent;
    DzEvent dataSendEvent;
    DzEvent connectEvent;
};

static struct IP_CONN s_ipConn;

static void lockConn4Zener(struct IP_CONN *conn)
{
    while (InterlockedExchange(&conn->lock, 1)) {
        while (conn->lock) { DzSleep(1, NULL); }
    }
}

static void lockConn4Nucleus(struct IP_CONN *conn)
{
    while (InterlockedExchange(&conn->lock, 1)) {
        while (conn->lock) { nu_sleep(1); }
    }
}

static void unlockConn(struct IP_CONN *conn)
{
    InterlockedExchange(&conn->lock, 0);
}

static void initQueue(struct IP_CONN *conn, int id)
{
    int i = id;
    conn->ipQueue[i].count = 0;
    conn->ipQueue[i].length =
        sizeof(conn->ipQueue[i].packet) / sizeof(conn->ipQueue[i].packet[0]);
    conn->ipQueue[i].put = conn->ipQueue[i].get = 0;
    conn->ipQueue[i].cid = 0;
    conn->ipQueue[i].state = disconnected;
    conn->ipQueue[i].fluxRecvCount = 0;
    conn->ipQueue[i].fluxSendCount = 0;
}

static void initConn(struct IP_CONN *conn)
{
    conn->count = sizeof(conn->ipQueue) / sizeof(conn->ipQueue[0]);
    conn->lock = 0;
    for (int i = 0; i < conn->count; i++) {
        initQueue(conn, i);
    }
}

static int getIdByCid(struct IP_CONN *conn, int cid)
{
    for (int i = 0; i < conn->count; i++) {
        if (conn->ipQueue[i].cid == cid) {
            return i;
        }
    }
    return -1;
}

static int allocIdByCid(struct IP_CONN *conn, int cid)
{
    for (int i = 0; i < conn->count; i++) {
        if (conn->ipQueue[i].cid == 0) {
            conn->ipQueue[i].cid = cid;
            return i;
        }
    }
    return -1;
}

static int freeIdByCid(struct IP_CONN *conn, int cid)
{
    for (int i = 0; i < conn->count; i++) {
        if (conn->ipQueue[i].cid == cid) {
            conn->ipQueue[i].cid = 0;
            return i;
        }
    }
    return -1;
}

static void cleanupQueue(struct IP_CONN *conn, int id)
{
    int i = id;
    while (conn->ipQueue[i].count > 0) {
        struct IP_PACKET *packet =
            &conn->ipQueue[i].packet[conn->ipQueue[i].get];
        if (packet->buffer) {
            nu_getTapiPPP()->ip_free(packet->buffer);
            packet->size = 0;
        }
        conn->ipQueue[i].get =
            (conn->ipQueue[i].get + 1) % conn->ipQueue[i].length;
        conn->ipQueue[i].count--;
    }
}

static void cleanupConn(struct IP_CONN *conn)
{
    for (int i = 0; i < conn->count; i++) {
        cleanupQueue(conn, i);
    }
}

static bool putQueue(struct IP_CONN *conn, int id, struct IP_PACKET *packet)
{
    if (conn->ipQueue[id].count < conn->ipQueue[id].length) {
        conn->ipQueue[id].packet[conn->ipQueue[id].put] = *packet;
        conn->ipQueue[id].put =
            (conn->ipQueue[id].put + 1) % conn->ipQueue[id].length;
        conn->ipQueue[id].count++;
        return true;
    }
    else {
        // drop
        nu_getTapiPPP()->ip_free(packet->buffer);
    }
    return false;
}

static bool getQueue(struct IP_CONN *conn, int id, struct IP_PACKET *packet)
{
    if (conn->ipQueue[id].count > 0) {
        *packet = conn->ipQueue[id].packet[conn->ipQueue[id].get];
        conn->ipQueue[id].get =
            (conn->ipQueue[id].get + 1) % conn->ipQueue[id].length;
        conn->ipQueue[id].count--;
        return true;
    }
    return false;
}

struct RAWIPIF_CONTROL {
    void (*connect)(
        unsigned char id,
        unsigned long ipaddr, unsigned long netmask, unsigned long gw,
        unsigned long dns1, unsigned long dns2);
    void (*disconnect)(unsigned char id);
};

extern "C" struct RAWIPIF_CONTROL g_ipif_control;

static void is_connected_by_TI(int cid, int is_connected)
{
    if (is_connected) {
        lockConn4Nucleus(&s_ipConn);
        int id = allocIdByCid(&s_ipConn, cid);
        if (id != -1) {
            s_ipConn.ipQueue[id].cid = cid;
            s_ipConn.ipQueue[id].state = connecting;
            cleanupQueue(&s_ipConn, id);
        }
        unlockConn(&s_ipConn);
    }
    else {
        lockConn4Nucleus(&s_ipConn);
        int id = getIdByCid(&s_ipConn, cid);
        if (id != -1) {
            s_ipConn.ipQueue[id].state = disconnecting;
            cleanupQueue(&s_ipConn, id);
            freeIdByCid(&s_ipConn, cid);
        }
        unlockConn(&s_ipConn);
    }
    nu_notifyDeviceCaller((Uint32)&s_ipConn.connectEvent, 1);
}

static int CDECL connection_state_waiting(void *dat)
{
    for (;;) {
        WaitResult wr;
        EventState stat = -1;
        s_ipConn.connectEvent.Wait(&wr, &stat);
        lockConn4Zener(&s_ipConn);
        for (int i = 0; i < s_ipConn.count; i++) {
            if (s_ipConn.ipQueue[i].state == connecting) {
                if (g_ipif_control.connect) {
                    unsigned long ip, mask, gw, dns1, dns2;
                    nu_getTapiPPP()->ip_get_addr(
                        s_ipConn.ipQueue[i].cid, &ip, &mask, &gw, &dns1, &dns2);
                    g_ipif_control.connect(
                        i, ip, mask, gw, dns1, dns2);
                    s_ipConn.ipQueue[i].state = connected;
                }
            }
            else if (s_ipConn.ipQueue[i].state == disconnecting) {
                if (g_ipif_control.disconnect) {
                    g_ipif_control.disconnect(i);
                    struct IP_PACKET packet = {NULL, 0};
                    putQueue(&s_ipConn, i, &packet);
                    s_ipConn.dataRecvEvent.Notify(1);
                    s_ipConn.ipQueue[i].state = disconnected;
                }
            }
        }
        unlockConn(&s_ipConn);
    }
    return 0;
}

static int recv_ip_from_TI(
    int cid, unsigned char *buffer, unsigned long size)
{
    struct IP_PACKET packet = {buffer, size};
    lockConn4Nucleus(&s_ipConn);
    int id = getIdByCid(&s_ipConn, cid);
    if (id != -1) {
        putQueue(&s_ipConn, id, &packet);
    }
    unlockConn(&s_ipConn);
    nu_notifyDeviceCaller((Uint32)&s_ipConn.dataRecvEvent, 1);
    return 0;
}

static void send_ip(int id, void *buffer_ptr, unsigned long size)
{
    if (nu_getTapiPPP()->ip_get_uplink_buf(
        s_ipConn.ipQueue[id].cid, size) != size) {
        WaitResult wr;
        EventState stat;
        s_ipConn.dataSendEvent.Wait(&wr, &stat);
    }
    nu_getTapiPPP()->ip_uplink(
        s_ipConn.ipQueue[id].cid, (unsigned char *)buffer_ptr, size);
    s_ipConn.ipQueue[id].fluxSendCount += size;
}

static void recv_ip(int *id, void **buffer_ptr_ptr, unsigned long *size)
{
    struct IP_PACKET packet;
    lockConn4Zener(&s_ipConn);
    for (int i = 0; i < s_ipConn.count; i++) {
        int signal = 0;
        if (s_ipConn.ipQueue[i].count == s_ipConn.ipQueue[i].length) {
            signal = 1;
        }
        if (getQueue(&s_ipConn, i, &packet)) {
            *id = i;
            *buffer_ptr_ptr = packet.buffer;
            *size = packet.size;
            s_ipConn.ipQueue[i].fluxRecvCount += packet.size;
            unlockConn(&s_ipConn);
            if (signal) {
                nu_getTapiPPP()->ip_notify_downlink_ready(
                    s_ipConn.ipQueue[i].cid);
            }
            return;
        }
    }
    *id = -1;
    *buffer_ptr_ptr = NULL;
    *size = 0;
    unlockConn(&s_ipConn);
    WaitResult wr;
    EventState stat = -1;
    s_ipConn.dataRecvEvent.Wait(&wr, &stat);
}

static void free_ip(void *buffer_ptr)
{
    nu_getTapiPPP()->ip_free(buffer_ptr);
}

static int query_recv_state_by_TI(int cid)
{
    int id = getIdByCid(&s_ipConn, cid);
    if (id == -1) { return 0; }
    return (s_ipConn.ipQueue[id].count < s_ipConn.ipQueue[id].length);
}

static void notify_send_ready_by_TI(int devid, unsigned char sig)
{
    /* GDD_SIGTYPE_BUF_SENT || GDD_SIGTYPE_SEND_BUF_AVAILABLE*/
    if (sig == 3 || sig == 4) {
        nu_notifyDeviceCaller((Uint32)&s_ipConn.dataSendEvent, 1);
    }
}

enum {
    RAWIP_IOCTL_FLUX_STATISTICS,
};

struct RAWIP_CONNECTION {
    UInt8 cid;
};

struct RAWIP_FLUX_STATISTICS {
    UInt8 cid;
    UInt32 fluxRecvCount;
    UInt32 fluxSendCount;
};

class RAWIP : public CDeviceDriver
{

public:
    CARAPI Read(
        /* [in] */ Int64 u64Offset,
        /* [in] */ Int32 bytesToRead,
        /* [out] */ MemoryBuf * pBuffer,
        /* [out] */ IEvent * * ppCompletionEvent)
    {
        return E_NOT_SUPPORTED;
    }

    CARAPI Write(
        /* [in] */ Int64 u64Offset,
        /* [in] */ const MemoryBuf & buffer,
        /* [out] */ Int32 * pBytesWritten,
        /* [out] */ IEvent * * ppCompletionEvent)
    {
        return E_NOT_SUPPORTED;
    }

    CARAPI Control(
        /* [in] */ Handle32 nControlCode,
        /* [in] */ const MemoryBuf & inBuffer,
        /* [out] */ MemoryBuf * pOutBuffer,
        /* [out] */ IEvent * * ppCompletionEvent)
    {
        if (nControlCode == RAWIP_IOCTL_FLUX_STATISTICS) {
            if (inBuffer.IsNullOrEmpty() ||
                inBuffer.GetUsed() < (Int32)sizeof(RAWIP_CONNECTION)) {
                return E_INVALID_ARGUMENT;
            }
            if (pOutBuffer == NULL ||
                pOutBuffer->GetCapacity() <
                    (Int32)sizeof(struct RAWIP_FLUX_STATISTICS)) {
                return E_BUFFER_TOO_SMALL;
            }
            RAWIP_CONNECTION *conn = (RAWIP_CONNECTION *)inBuffer.GetPayload();
            RAWIP_FLUX_STATISTICS *statistics =
                (RAWIP_FLUX_STATISTICS *)pOutBuffer->GetPayload();
            statistics->cid = conn->cid;
            statistics->fluxRecvCount = 0;
            statistics->fluxSendCount = 0;
            if (conn->cid == 0) {
                for (int i = 0; i < s_ipConn.count; i++) {
                    statistics->fluxRecvCount +=
                        s_ipConn.ipQueue[i].fluxRecvCount;
                    statistics->fluxSendCount +=
                        s_ipConn.ipQueue[i].fluxSendCount;
                }
            }
            else {
                int i = 0;
                for (; i < s_ipConn.count; i++) {
                    if (conn->cid == s_ipConn.ipQueue[i].cid) {
                        statistics->fluxRecvCount +=
                            s_ipConn.ipQueue[i].fluxRecvCount;
                        statistics->fluxSendCount +=
                            s_ipConn.ipQueue[i].fluxSendCount;
                        break;
                    }
                }
                if (i == s_ipConn.count) {
                    return E_INVALID_ARGUMENT;
                }
            }
            return NOERROR;
        }
        return E_NOT_SUPPORTED;
    }

    virtual void Dispose()
    {
        lockConn4Zener(&s_ipConn);
        cleanupConn(&s_ipConn);
        unlockConn(&s_ipConn);
        delete this;
    }
};

EXTERN IDeviceDriver * CDECL  CreateRawIP(
    /* [in] */ uint_t deviceNo,
    /* [in] */ void* parameter)
{
    extern void lwipentry(void);
    lwipentry();

    initConn(&s_ipConn);
    nu_getTapiPPP()->ip_init(
        is_connected_by_TI,
        recv_ip_from_TI,
        query_recv_state_by_TI,
        notify_send_ready_by_TI);
    tcpip_rawip_set_handler(send_ip, recv_ip, free_ip);

    DzCreateKernelThread(
        connection_state_waiting, NULL, CreateThreadFlag_System,
        ThreadPriorityRank_Normal, NULL);
    RAWIP *newRawIP = new RAWIP;
    return newRawIP;
}

